feat(runtime, spec): AppPlugin supplies SeedLoaderConfig.locale, so the Seed.locale axis takes effect on the default boot path - #17013
Conversation
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 86ea7cc8b5ca09d05cda8cb4c83e5e5a4aca0cbf && git checkout 86ea7cc8b5ca09d05cda8cb4c83e5e5a4aca0cbf
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d127f9babce9d483ac4c053194360968e1c7ba0a 06a16c71839eb48d9003a3e80009cdab7632fe32 && git checkout -B drift-repro d127f9babce9d483ac4c053194360968e1c7ba0a && git merge --no-ff 06a16c71839eb48d9003a3e80009cdab7632fe32
node scripts/docs-audit/affected-docs.mjs --json d127f9babce9d483ac4c053194360968e1c7ba0a
|
…does not, and flag the release-note reconciliation
|
Docs Drift Check — all ten rows re-verified. Details are in the PR body under Docs drift; summary here so it sits beside the bot comment. ⛔ My first pass reported a zero that was not an answer to the question the bot asked: I swept Answered on the bot's tree: 8 hand-written rows — 4 true, 4 wrong. None falsified, so no edit is owed on any of them.
⛔ 2 release-owned pages — audited read-only, NOT edited, and neither is wrong.
That sweep also found one live claim outside Generated by Claude Code |
#17013 falsified `packages/spec/CHANGELOG.md` and `packages/metadata-protocol/CHANGELOG.md` carry the same compiled 17.4.0 entry for the `Seed.locale` axis. Two of its present-tense claims were true when written and false by the time the entry was published: "no first-party call site supplies one" and "the liveness ledger records `seed.locale` as `experimental`". Both are corrected to the post-#17013 truth, each followed by one erratum line naming PR #17013 and the correction date, per the maintainer ruling recorded on #17026 (director decision batch #119 item 1, 2026-09-12). The clause "on the default boot path the axis is inert" is falsified by the same landing and is corrected in the same sentence; the residual bound is stated rather than dropped — three request builders outside `AppPlugin` still pass no locale. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…jectstack-ai#17013 falsified, with erratum lines (objectstack-ai#17896) Fixes objectstack-ai#17026 Clause-②: no — the landing surface is prose inside two already-compiled `packages/*/CHANGELOG.md` entries plus erratum lines. No schema key, no accepted-set change, no new exported symbol, no new key on a published payload. Diff is markdown only; no path under `packages/**/src/**` moves. ## The ruling this PR implements, verbatim Director seat, decision batch objectstack-ai#119 item 1, 2026-09-12T04:15Z. Maintainer's own word to the batch presented as `1A·2A·3A·4D·5A`: 「同意」. > **A: correct the two published CHANGELOG entries in the repository, with a post-release erratum note.** > > 1. `packages/spec/CHANGELOG.md` and `packages/metadata-protocol/CHANGELOG.md`: the two `Seed.locale` sentences ("no first-party call site supplies one", "the liveness ledger records `seed.locale` as `experimental`") are corrected to the post-objectstack-ai#17013 truth, each followed by **one erratum line naming PR objectstack-ai#17013 and the correction date**. > 2. **Before editing**, run the release tooling's dry-run (`changeset version` on a scratch branch) and record that a manual edit inside an already-compiled section **survives** — the triage seat's open confidence gap; **if it does not survive, stop and report**. > 3. `skip-changeset`: no package publishes anything from this change. And the boundary that makes A legitimate, also verbatim: > Immutability belongs to the **shipped npm tarball**, which this ruling does not and cannot touch; it does not belong to the repository's markdown. ## Step 2 first — the gate, run before any edit Run on a local scratch branch off the same base, then discarded. Marker inserted into the already-compiled `## 17.4.0` section of both files (into the very paragraph this PR edits), then `pnpm changeset version`. | reading | value | |:--|:--| | marker count before edit | spec **0**, metadata-protocol **0** | | marker count after edit, on disk | spec **1**, metadata-protocol **1** (`git diff --stat` = 2 files, 2 insertions / 2 deletions) | | `pnpm changeset version` | `VERDICT changeset-version-exit 0` — "All files have been updated" | | **marker count AFTER `changeset version`** | spec **1**, metadata-protocol **1** — **the manual edit SURVIVES** | | where the marker ended up | spec `2451` → `5654`; metadata-protocol `128` → `898` — pushed down, content intact | | **lit control** — did the tool actually rewrite the files? | yes: a new `## 17.5.0` section is prepended at the top, `packages/spec/package.json` moves `17.4.0` → `17.5.0`, and the 250 pending changesets drop to **0** | | restore | `git reset --hard` + `git clean -fd`; `git diff HEAD` empty, 0 marker residue, 250 changesets back, scratch branch deleted | ⇒ `changeset version` **prepends** and never rewrites an already-compiled section. The triage seat's own stated blind spot — "我**没有验证过手工编辑 CHANGELOG 会不会被下一次发版覆盖或冲突** …… 但我**没有测**" — is now measured, and it does not block A. ## Step 1 — what changed, and why each corrected sentence is true of today's tree Both files carry the same compiled `## 17.4.0` entry for commit `c1d8f98`, byte-identical before and after this change. The single paragraph at `packages/spec/CHANGELOG.md:2451` / `packages/metadata-protocol/CHANGELOG.md:128` becomes four: two corrected paragraphs, each followed by one erratum line naming PR objectstack-ai#17013 and the correction date 2026-09-12. **Claim 1 — "no first-party call site supplies one".** Verified false on this tree, not taken from the card: - `packages/runtime/src/app-plugin.ts:1717` declares `private resolveSeedLocale()`, reading `i18n.defaultLocale` off the bundle. - It is threaded into all three `SeedLoaderRequest`s that plugin builds: lines `1349`, `1434`, `1791` (`...(seedLocale ? { locale: seedLocale } : {})`). ⇒ the corrected sentence says a first-party call site **does** supply `config.locale` and names the producer and the three sites. **Claim 2 — "the liveness ledger records `seed.locale` as `experimental`".** Verified false: `packages/spec/liveness/seed.json` → `props.locale.status` reads **`live`**, with `producer` naming `packages/runtime/src/app-plugin.ts#resolveSeedLocale`. **One adjacent clause corrected in the same stroke, named rather than smuggled.** "on the default boot path the axis is inert" sits in the same paragraph and is falsified by the same landing. Leaving it would have left a known-false clause beside a corrected one, which defeats the ruling's own purpose; it is corrected and called out here. **The corrected text does not overclaim.** The ledger row's `live` is explicitly bounded, and the correction carries that bound rather than dropping it. Measured on this tree: three request builders outside `AppPlugin` still build a `config` with no `locale` key — - `packages/runtime/src/domains/packages.ts:1776` `applyPublishedSeeds` (package apply) — `config: { defaultMode, multiPass, organizationId? }`, zero occurrences of `locale` in the whole file; - `packages/metadata-protocol/src/protocol.ts:17490` `applySeedBodies` (draft publish) — same three keys; - `packages/cloud-connection/src/marketplace-install-local-plugin.ts:1510` `runInlineSeed` (marketplace install) — same three keys. And `SeedLoaderService#warnOnUnresolvedLocaleScope` still exists (`packages/metadata-protocol/src/seed-loader.ts:2592`, called at `:554`), so the corrected paragraph keeps the sentence about the warning instead of deleting it. ## House convention followed rather than invented The dispatch pointed at objectstack-ai#16859 as the precedent.⚠️ Read to the end, **objectstack-ai#16859 did not in fact edit a published CHANGELOG** — its own report states `packages/spec/CHANGELOG.md` "was not opened for writing" and that correction 1 was "confirmed foreclosed and left alone". So it establishes no erratum wording. The convention that does exist was found by measurement — three entries already corrected in place in this repo: - `packages/spec/CHANGELOG.md:5257` — "As published, this sentence justified that by … that was false when written and is false now …" closing `(Corrected after publication, objectstack-ai#15058.)` - `packages/spec/CHANGELOG.md:7401` and `packages/lint/CHANGELOG.md:1214` — same shape, closing `(Corrected after publication, objectstack-ai#15026.)` ⇒ this PR follows that: the published sentence is quoted in place and marked false, the correction follows in the same entry, and the last erratum line closes `(Corrected after publication, objectstack-ai#17026.)` naming the card, as those three name theirs. It is also exactly what the `packages/*/CHANGELOG.md` guardrail row now added to AGENTS.md by objectstack-ai#16849 prescribes: "Factual error in a released entry → **amend that entry in a dedicated docs-only PR**, ⛔ never an erratum in a later entry and never a rider on code changes". This PR is docs-only, amends the entry itself, and adds nothing to a later entry. ## `skip-changeset` — measured, not asserted | reading | value | |:--|:--| | diff file list vs merge base `c744c0af332` | exactly two paths, both `CHANGELOG.md` | | any non-markdown path | **none** | | `.changeset/*.md` added by this PR | **0** | | `packages/spec` `files[]` | `dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json` | | `packages/metadata-protocol` `files[]` | `dist, README.md, CHANGELOG.md` |⚠️ Stated precisely so neither half misleads: `CHANGELOG.md` **is** in both packages' `files[]`, so the corrected bytes do ship inside the next tarball either package happens to publish. What does **not** move is any package **version**, any **API**, any `dist/`, any source file, or any generated artifact — and because this PR adds no changeset, it schedules no version bump of its own. That is the sense in which "no package publishes anything from this change" holds. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` against the real change set, then reconciled with `--ran`. - **54 derived families — 50 run green, 4 NOT MEASURED, 0 unrun.** Reconciliation verdict, verbatim: `✓ dispatch-gates --ran: 54 derived famil(ies) accounted for — 50 run, 4 NOT-MEASURED (4 DERIVED from a recorded exit 3).` Every exit code captured before any pipe. - The 4 NOT MEASURED are `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`; all exit **3** = PREREQUISITE NOT MET, each refusing because there is no `dist/` in this worktree ("Run `pnpm build` first. ⛔ This is NOT a pass: nothing was measured."). They read build artifacts only, which a markdown-only diff cannot move. **Declared to CI**, which builds first. - Repo-wide `pnpm lint` (`eslint . --no-inline-config`) over the **full** population: exit **0** in 1m26s. No narrowing declared because none was needed. A targeted run over just these two files reports `File ignored because no matching configuration was supplied.` for both — markdown is outside eslint's configured population here. - `pnpm check:nul-bytes` green, plus a manual control-character sweep of both touched files (`grep -naP` over the non-tab, non-newline C0 range plus DEL): **no matches**, with a printable lit control on the same files firing. - Package test suites are **not owed and were not run**, stated rather than silently skipped: the diff compiles into nothing and is imported by nothing. The only test that mentions `CHANGELOG.md` as a file class is the `action-owner-key-single-source` pair, whose scanner is fixed to `.ts` sources. ## Acceptance notes — out of scope, noted, not filed - `packages/core/src/plugin-contract.ts:69` carries a code comment asserting the opposite posture for a different entry: "⚠️ `CHANGELOG.md` is NOT a live document and is deliberately not corrected … supersedes them BY VERSION rather than by rewriting them." That predates the `packages/*/CHANGELOG.md` guardrail row objectstack-ai#16849 added to AGENTS.md, which now prescribes amending the entry. ⛔ Not touched by this PR and not filed — it belongs to whoever next revisits that entry, and the governing text is already the AGENTS.md row. ## Not in play ⛔ `content/docs/releases/` is untouched by this PR and by all of the ruling's options. This is `packages/*/CHANGELOG.md`, a different surface. Named here only so the red line is not read into a task it does not cover. Authored by the `domain:spec` execution lane, session `session_01MkQhmuuJAVDjmeWNixwDDH`, on branch `claude/issue-17026-seed-locale-changelog-erratum`. --- _Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_ Co-authored-by: Claude <noreply@anthropic.com>
…ectstack-ai#17783/objectstack-ai#17785 (objectstack-ai#18077) Fixes objectstack-ai#18076 Clause-②: no — the landing surface is prose inside one already-compiled `packages/spec/CHANGELOG.md` entry plus an erratum line. No schema key, no accepted-set change, no new exported symbol, no new key on a published payload, and no path under `packages/**/src/**` moves. `skip-changeset`: no package publishes anything from this change. ## The ruling this PR implements Director seat, decision batch objectstack-ai#119 item 1, 2026-09-12T04:15Z, maintainer's own word `1A·2A·3A·4D·5A`: 「同意」, implemented once already by `ef47459474` (objectstack-ai#17896). This card applies the same remedy shape to a second falsified instance: > **A: correct the two published CHANGELOG entries in the repository, with a post-release erratum note.** … the … sentences … are corrected to the post-objectstack-ai#17013 truth, each followed by **one erratum line naming PR objectstack-ai#17013 and the correction date**. > > **Before editing**, run the release tooling's dry-run (`changeset version` on a scratch branch) and record that a manual edit inside an already-compiled section **survives** … **if it does not survive, stop and report**. > > `skip-changeset`: no package publishes anything from this change. ## Step 2 first — the survival gate, run before any edit On a local scratch branch off `484484041a` (`scratch/issue-18076-survival-gate`), discarded afterward: | reading | value | |:--|:--| | marker inserted into the already-compiled `## 17.4.0` "Keys deliberately left alone" paragraph | `SURVIVALGATEMARKER18076XQ7` | | marker count before `changeset version` | **1** | | dark control (`ZZQV18076NOPE`, invented at read time) before | **0** | | `pnpm changeset version` | exit 0, "All files have been updated" | | marker count **after** `changeset version` | **1** — the manual edit **survives** | | where the marker ended up | line 2869 → line 7316 — pushed down, content intact | | dark control after | **0** — unchanged, confirming the grep methodology (not a broken instrument reading a false zero) | | lit control — did the tool actually rewrite the files? | yes: a new `## 17.5.0` section is prepended, `packages/spec/package.json` moves `17.4.0` → `17.5.0`, and 452 of 453 pending `.changeset/*.md` files are consumed | | restore | `git checkout -f` back to the real branch; `diff <(git show origin/main:packages/spec/CHANGELOG.md) packages/spec/CHANGELOG.md` → **identical**; scratch branch deleted | ⇒ `changeset version` prepends a new section and never rewrites an already-compiled one. Re-measured on this tree rather than inherited from `ef47459474`'s reading, per the ruling's explicit instruction. ## What changed, and why each corrected claim is true of today's tree `packages/spec/CHANGELOG.md`, inside the already-released `## 17.4.0` entry (commit `e9fcd6b`, "Keys deliberately left alone"), said: > `ServiceLevelObjective.errorBudget`'s burn-rate `window` and the OpenTelemetry exporter `timeout` name no unit anywhere in their prose, so both are outside the gate's population entirely. Pin tests assert each of these, so a later sweep cannot read this card as "every duration-shaped number on these files". All three claims are false, verified on this tree independently of the card: - **Half 1** — `packages/spec/src/system/metrics.zod.ts:368-375` (`SLO_BURN_RATE_WINDOW_RETIRED`): `ServiceLevelObjective.errorBudget.burnRateWindows[].window` was renamed to `durationSeconds` in `@objectstack/spec` 17 (objectstack-ai#17783), tombstoned with `retiredKey()` at `metrics.zod.ts:402`/`:498`. The pin that held the bare spelling is now `metrics.test.ts:612`, `it('REFUSES the burn-rate \`window\` with a rename naming \`durationSeconds\`', …)`. - **Half 2** — `packages/spec/src/system/tracing.zod.ts:546-550`: `OpenTelemetryCompatibility.exporter.timeout` was renamed to `timeoutMs` (objectstack-ai#17785), tombstoned with `retiredKey()`; its `batch.exportTimeout` sibling moved to `exportTimeoutMs` the same way (`:586`/`:601`). The pin is now `tracing.test.ts:594-611`, `it.each([['exporter.timeout', 'timeoutMs'], …])('REFUSES the retired \`%s\` with the rename in the message', …)`. The corrected paragraph quotes the falsified sentence, states the truth with both PR numbers and both new key names, and closes with one erratum line naming this PR and the correction date, matching `ef47459474`'s shape. **The paragraph's other four claims were checked and are still true** (not touched, per the ruling's scope boundary): - `FailoverConfig.dns.ttl` — still bare, `disaster-recovery.zod.ts:148`, under the same `externalVocabulary` exemption comment. - `CacheAvalanchePrevention.lockout.lockTimeoutMs` — still milliseconds, `cache.zod.ts:162`. - `MetricExportConfig.batch.size` — still a bare record count, `metrics.zod.ts:702`. - `QueueConfig.rateLimit.max` — still a bare task count, `worker.zod.ts:~276`. ## The erratum line's PR number — how it went in, and the fix for a review finding `objectstack-ai#18077` (the PR number for this correction) is not knowable before the PR exists, so the first pushed commit (`bfc744c98c`) carried a placeholder, `#PRPLACEHOLDER18076`, in the erratum line's parenthetical, meaning to fill it in once the PR was open. **That fill-in commit (`ac68eff364`) was made locally but not pushed before the round's first status check ran against `bfc744c98c`, so a reviewer correctly caught a shipped placeholder.** It is now pushed; **HEAD is `ac68eff364`**, verified directly against the remote rather than assumed: ``` $ git show origin/claude/issue-18076-changelog-duration-keys-erratum:packages/spec/CHANGELOG.md | grep -c PRPLACEHOLDER 0 $ git show origin/claude/issue-18076-changelog-duration-keys-erratum:packages/spec/CHANGELOG.md | grep -n 'Corrected after publication, objectstack-ai#18077' 7331: population as of `@objectstack/spec` 17. (Corrected after publication, ``` **Placeholder sweep of the whole diff, with controls** (`git diff origin/main..HEAD -- packages/spec/CHANGELOG.md`, 35 lines): - Pattern `PLACEHOLDER|\bTODO\b|\bTBD\b|\bXXX\b|\bFIXME\b|<[A-Z_]+>|\{\{.*\}\}` (case-insensitive) over the diff: **0 matches**. - Lit control, drawn a different way: appended a synthetic `PRPLACEHOLDER99999` line to the same scanned text and re-ran the identical pattern — **1 match**, confirming the regex class actually fires and the 0 above is a real zero, not a typo'd pattern reading a false clean. - Positive control: `18077` appears **exactly once** in the diff — the one erratum reference, present and correctly substituted. ## Verification `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **54** local gate families for this diff (all matched via broad `packages/**` / `packages/spec` globs — this path carries no gate of its own). Ran all 54 **twice** — once on `bfc744c98c`, again on the final `ac68eff364` after the placeholder fix landed — both times reconciled with `--ran` against the recorded exit codes, same result both runs: ``` Run reconciliation — 54 derived, 50 run, 4 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 54 derived famil(ies) accounted for — 50 run, 4 NOT-MEASURED (4 DERIVED from a recorded exit 3). ``` - **50/50 that ran, passed** (`pnpm check:nul-bytes`, `check:release-notes`, `check:release-page-status`, `check:release-section-coverage[--strict]`, `check:changeset-gate-self-tests`, `check:doc-authoring`, `check:page-declaration-shape`, `check:published-files`, `check:slot-lookup`, `check:test-source-alias`, `check:type-source-resolution`, and 39 more — full list in this branch's derivation). - **4 NOT-MEASURED, exit 3 (`PREREQUISITE NOT MET`), declared rather than swallowed**: `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content` — each refuses on a missing `dist/` in this fresh worktree ("Run `pnpm build` first … this is NOT a pass: nothing was measured"). These are whole-repo build-completeness gates unrelated to this diff's content (no code, no export, no `dist` path moves); running a ~80-package `pnpm build` to satisfy them is disproportionate to a one-line CHANGELOG correction, and CI builds fresh before running them. - No changeset added; `check:changeset-gate-self-tests` (the gate's own self-test) is green. `skip-changeset` now shows as an applied label on this PR (not applied by this session — labels were left untouched per this card's operating constraints). - `pnpm check:nul-bytes` and a direct `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` both clean on the diff. - `node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD`: clean on both commits — no card-relation trailer, model-free trailer pair. No `packages/spec` test/typecheck run beyond the above: this diff touches no code, no export and no public surface byte, so per this repo's verification-scope convention there is nothing for `pnpm --filter @objectstack/spec test`/`typecheck` to newly exercise. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt --- _Generated by [Claude Code](https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #16595
Seed.locale's consumer landed complete in #16510 / PR #16592; its producer did not exist. No first-party call site passedSeedLoaderConfig.locale, sofilterByLocalereturned its input on its first line anddataset.localewas never read at all on the default boot path. This supplies the producer, then flips the ledger row.What landed
AppPlugin#resolveSeedLocalereads the app's declaredi18n.defaultLocale— the same envelope key, read the same wayloadTranslationsalready reads it forsetDefaultLocale— and threads it into all threeSeedLoaderRequests this plugin builds:packages/runtime/src/app-plugin.ts(registered for tenant provisioning)packages/runtime/src/app-plugin.tspackages/runtime/src/app-plugin.tsAn app that declares no locale sends no
localekey at all rather than an'en'default. Absence is the loader's unrestricted spelling, so a stack that never opted in keeps loading every dataset exactly as before; a default would have turned a wiring change into a data change, silently dropping alocale: ['zh-CN']dataset on every stack without ani18nblock. A blank or non-stringdefaultLocaleis treated as absence for the same reason.Why the locale is resolved at the call sites and not inside
load(). The card asked me to confirm this shape againstresolveEnvConfig, and confirming it changed the answer:envis not resolved at its call sites either — #4704 moved it insideload(), the one funnel, precisely so "call site seven cannot reopen the hole". That remedy does not transfer.resolveEnvConfigcan read an ambient, process-wideNODE_ENV; a locale has no ambient source, and the only layer that knows which locale a stack runs in is the app config the loader is never handed. So this axis genuinely needs a producer at the call sites.The ledger flip, and the exact scope of its
livepackages/spec/liveness/seed.json→seed.locale:experimental→live, with aproducerpointer namingAppPlugin#resolveSeedLocale.state-counts.mdregenerated bygen:liveness-counts— never hand-edited (seedlive 12 → 13, experimental 1 → 0; total live 850 → 851, experimental 6 → 5).Is
livean overclaim at three of six? Stated so it can be disagreed with. The ledger's own criterion is "authoring the property changes runtime behaviour", and theproducerfield exists so that claim is not made on a consumer pointer alone. After this change, authoringlocaleon adefineStack()app that declaresi18n.defaultLocalechanges what gets seeded — proven by the ablation below, not by a passing suite. The #4837 specimen was false in the only sense the criterion has:Seed.envwaslivewhile zero of six builders supplied it, so authoring changed nothing anywhere. This is three of six including the entire default boot path, with a real producer cited. Soliveis correct and bounded, and the row now spells the bound out in its own words — what it asserts, and, marked with a ⛔, what it does not: that it is NOT a claim that every path reachingload()supplies a locale. If a reviewer reads that bound as still too generous, reverting toexperimentalwith the corrected six-site enumeration is a coherent alternative and I would not argue it is wrong — but it would understate a producer that demonstrably bites.envrow one entry up enumerates. This wires the three on the default boot path — which is what makes authoringlocalechange runtime behaviour, and therefore what makes the rowlive. The other three are publish/install-time paths, are handed no stack config, and still pass no locale. They are listed in the note by symbol, and filed as #17011.protocol.tscall site: re-derived, and the card's doubt resolvedThe card said PR #16592's body claims a draft-publish call site in
protocol.tswhile the filing seat's grep atc383352found none, and told me to trust neither reading.PR #16592's body was right.
packages/metadata-protocol/src/protocol.ts→applySeedBodiesbuilds aSeedLoaderRequestand callsloader.load(parsedRequest.data).The earlier negative was an anchor artefact, not an absence: a grep for
seedLoader.loadcannot match that site, because it names its localloader. Re-deriving onSeedLoaderRequestSchemainstead — the shape every builder must use — found six, not three, and not four:The census, in a form that can be re-driven verbatim. Instrument, corpus and controls, at head
06a16c7183:Six builders, minus the schema's own definition file:
The
seedLoader.loadanchor returns 4 of those 6: it missesprotocol.tsanddomains/packages.ts, both of which name their localloader. That is the whole artefact, and it is why the census is anchored on the schema rather than on a variable name.⭐ The pre-flip ledger note itself named only four of the six. That enumeration is corrected in this PR.
warnOnUnresolvedLocaleScopeSTAYS — the judgement, with its reason⛔ Not deleted, and not kept by default. It is not a signpost for an unwired state that has now gone away:
load()with no locale;i18nblock deliberately sends none.Every one of those is a live population, and the warning is the only thing standing between them and a silent no-op — which is the property this row spent a release having. Deleting it would re-create the #4837 failure one layer down.
Evidence — the negative leg, not a green suite
⛔ "The tests pass" is not a measurement here: a green suite is equally consistent with the axis still being inert, which is exactly how
Seed.envstayed green for releases while unwired. So the new coverage asserts on rows that reach the engine, and it is verified by ablation.New file
packages/runtime/src/app-plugin.seed-locale-producer.test.ts— 7 cases, covering all three call sites (⛔ notseed-loader.test.ts, held by PR #16783).Ablation (neutralise
resolveSeedLocaletoreturn undefined, i.e. the pre-change state):Ablation and restore both ran from a committed state under a
trap ... EXIT INT TERMwith absolute paths. No permanent ablation artefact is left in the tree.Verification
Everything below was measured at head
06a16c7183(post-merge oforigin/main; the gate union was re-run after the drift work).pnpm --filter @objectstack/runtime typecheck— exit 0;check:test-typecheck: OK.pnpm --filter @objectstack/runtime exec vitest run --project local— exit 0, 249 files / 3516 tests passed.pnpm --filter @objectstack/spec check:generated— exit 0, all 15 artifacts up to date.scripts/pm/dispatch-gates.mjs --ran, re-derived on the merged head after the docs edit pulled in the docs families). 96 green; 2 returned exit 3 = PREREQUISITE NOT MET, read as NOT MEASURED, not as green:check:dual-build-cjs-loads— needs a whole-workspace build. This diff adds no export entry, so it cannot move.check:type-check-debt --re-measure— needs the built closure. Its DEBT ledger holdscloud-connection,hono,observability,spec-monorepo; this diff touches none of them.check:engine-double-contractreportedRETAINED [findOne]for the new test's engine double and asked for the ledger row; written with the gate's own--writeand re-run exit 0 (846 pinned). The double declares neitherdeletenorupdate— the seeds areupsertinto an empty store, so neither dispatch verb is reachable.Lint — a declared, measured narrowing, not a skipped run. The repo-wide
eslint . --no-inline-configis CI's; this PR ran the affected files and proves the narrowing excluded nothing:eslint.config.mjs's blocks target**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}andpackages/**/*.{ts,tsx,mts,cts}. This diff's only files matching any block'sfilesglob are the two below; the rest are.json/.md/.mdx, which no block names.--format json: 2 files, 0 errors, 0 warnings, exit 0.eslint.config.mjsstates it in its own words — "this repo runs oneeslint.config.mjs, which never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file, test or not", recorded there with a positive control. So no untouched file's verdict is a function of this diff.Changeset — measured, not guessed. ⛔ No
skip-changeset: both packages ship changed bytes.npm pack --dry-run --jsoninpackages/spec: 2073 shipped files;liveness/seed.jsonandliveness/state-counts.mdare both present. Positive controlpackage.jsonpresent; negative controlscripts/absent.packages/runtimebuilds todist, which is its wholefiles[];resolveSeedLocaleis indist/index.js,dist/index.cjs,dist/index.d.tsanddist/index.d.ctsafterpnpm --filter @objectstack/runtime build. Positive controlclass AppPluginpresent in the declarations.So
.changeset/seed-locale-producer-wiring.md:@objectstack/runtimeminor,@objectstack/specpatch.check:changeset-no-majorexit 0.Docs drift — the bot's answer, not mine. ⛔ My first pass reported a zero that was not an answer to the question the bot asked: I swept⚠️ Its footer warns its checkout carried uncommitted changes, so its sha is not cited here as a pin — the per-file blob equality is.
config.locale/SeedLoaderConfig/warnOnUnresolvedLocaleScope/locale-scoped/Seed.locale, and the bot anchors onAppPlugin— the class this diff adds a method to — which is disjoint from every token I swept. The bot's ten rows are re-verified below. Its treee6f974cb0band my worktree hold byte-identical blobs for all ten pages (git rev-parseon each path, compared), so the audit is answered on the tree the bot read.content/docs/kernel/services-checklist.mdxi18n.defaultLocalenow has a second consumer, but the list does not claim to enumerate every consumer. Considered adding a cross-reference and declined: it would widen this PR into a page whose subject is the i18n service.content/docs/protocol/kernel/index.mdxcontent/docs/protocol/kernel/lifecycle.mdxAppPlugin'soptionalDependenciesandrequiresServices; the delivered diff changes neither, and the quoted excerpt still matches the class.content/docs/protocol/kernel/plugin-spec.mdxonEnableseamAppPlugin.start()invokes; untouched.content/docs/permissions/authentication.mdxAppPluginoccurs once, in animportline of a mock-mode config example. Nothing about seeds, locale orstart().content/docs/permissions/capabilities.mdxcontent/docs/plugins/index.mdxos devregisters.content/docs/plugins/packages.mdxReported rather than silently dropped, as the bot asks: 4 of 8 hand-written rows are wrong, all from the same cause — a class-level anchor on
AppPlugincannot distinguish a page that documents the class's behaviour from one that names it in an import or a catalog list. Four of them are true rows and each was read; none is falsified, so no edit is owed on those eight.⛔ Release-owned pages, audited read-only and NOT edited:
content/docs/releases/v15.mdxnamesAppPluginonce, in the ADR-0087 protocol-handshake entry;content/docs/releases/v17.mdxnames it once, in theonEnableapp-bundle contract entry. Neither is touched by this diff and neither is wrong, so there is nothing to file and no docs-only PR is owed.packages/spec/liveness/seed.jsonandstate-counts.mdyielded no anchor, so pages documenting them were invisible to that run — and theexperimental→liveflip is exactly such a claim. Sweep ofcontent/forliveness/state-counts/seed.locale/not wired/axis is inert/experimental: 118 lines across many pages (so the sweep fires; the coarse controldefineSeedalso matches), and not one describes theseed.localerow or its status. The only page that stated the axis's wiring state wascontent/docs/data-modeling/seed-data.mdx, whose callout told readers some host must supplyconfig.localewithout saying the platform now does — that page is falsified by this change, is hand-written and not release-owned, and is corrected here.content/, found by that same sweep and deliberately not edited. The sibling changeset.changeset/seed-locale-axis.md(from the PR that landed the consumer half, unreleased) states in the present tense that no first-party call site suppliesconfig.locale, that the axis is inert on the default boot path, and that the ledger recordsseed.localeasexperimental. This change ends all three. ⛔ It is not rewritten here — it accurately records what its own PR did, and release notes are compiled centrally at release time — so the reconciliation is called out in this PR's own changeset instead, where the compiler cannot miss it. (packages/spec/src/data/seed-loader.zod.ts's "the locale axis is inert" is conditional on "when not specified" and stays true.)⛔ No page under
content/docs/releases/is edited.Clause-②: no
Re-derived from the DELIVERED diff, not copied from the dispatch. This supplies a producer, adds a test, flips one liveness row, adds one pinned-ledger row, writes a changeset and corrects one hand-written docs page. It relaxes no accepted set, widens no schema, adds no closed-set member, withdraws no capability and migrates no stored data shape —
localewas already onSeedSchemaandSeedLoaderConfigSchemabefore this branch. The behavioural direction is the opposite of widening: a locale-scoped dataset that used to load everywhere now loads only where it is declared. The one accepted set that grows isscripts/engine-double-contract.pinned.json, which is new coverage, written by the gate's own--write.pnpm check:pm-widening-tellsexit 0.验收备注
Findings from this work that are not filed, with the seat that will meet them named:
noted, not filed: the pre-flip seed.locale ledger note enumerated four of the six SeedLoaderRequest builders, and one of the four was called unconfirmed when it is real. Corrected in this PR rather than filed. 承接者: whoever next re-verifies this row — the corrected enumeration and the reason the old grep missed two sites are both in the note.noted, not filed: a grep for seedLoader.load is not a call-site census for this family — two of the six builders name their local loader. 承接者: #17011's implementer, who needs the same census; the working expression is recorded in that card's body.Filed as its own card:
localescope is silently inert on the three publish/install-time seed-load paths — package apply, draft publish and marketplace install pass noconfig.locale#17011 — the three publish/install-time seed-load paths pass noconfig.locale, so alocale-scoped dataset applied through package apply, draft publish or marketplace install loads for every locale. Filed unassigned and ungraded, with the three decisions its implementer must answer first.⛔ Out of scope and untouched, as the card requires: reconciling rows already written under a different locale — #16596.
Generated by Claude Code